/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.taobao.weex.ui.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Message;

import androidx.core.view.NestedScrollingChild;
import androidx.core.view.NestedScrollingChildHelper;
import androidx.core.view.ViewCompat;

import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ScrollView;

import com.taobao.weex.ui.component.WXComponent;
import com.taobao.weex.ui.component.WXScroller;
import com.taobao.weex.ui.view.gesture.WXGesture;
import com.taobao.weex.ui.view.gesture.WXGestureObservable;
import com.taobao.weex.utils.WXLogUtils;
import com.taobao.weex.utils.WXReflectionUtils;
import com.taobao.weex.common.WXThread;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;


/**
 * Custom-defined scrollView
 */
public class WXScrollView extends ScrollView implements Callback, IWXScroller,
        WXGestureObservable, NestedScrollingChild {

    private NestedScrollingChildHelper childHelper;
    private float ox;
    private float oy;
    private int[] consumed = new int[2];
    private int[] offsetInWindow = new int[2];

    int mScrollX;
    int mScrollY;
    private WXGesture wxGesture;
    private List<WXScrollViewListener> mScrollViewListeners;
    private WXScroller mWAScroller;
    //sticky
    private View mCurrentStickyView;
    private boolean mRedirectTouchToStickyView;
    private int mStickyOffset;
    private boolean mHasNotDoneActionDown = true;
    @SuppressLint("HandlerLeak")
    private Handler mScrollerTask;
    private int mInitialPosition;
    private int mCheckTime = 100;
    /**
     * The location of mCurrentStickyView
     */
    private int[] mStickyP = new int[2];
    /**
     * Location of the scrollView
     */
    private Rect mScrollRect;
    private int[] stickyScrollerP = new int[2];
    private int[] stickyViewP = new int[2];
    private boolean scrollable = true;

    public WXScrollView(Context context) {
        super(context);
        mScrollViewListeners = new ArrayList<>();
        init();
        try {
            WXReflectionUtils.setValue(this, "mMinimumVelocity", 5);
        } catch (Exception e) {
            WXLogUtils.e("[WXScrollView] WXScrollView: ", e);
        }
    }

    private void init() {
        setWillNotDraw(false);
        startScrollerTask();
        setOverScrollMode(View.OVER_SCROLL_NEVER);
        childHelper = new NestedScrollingChildHelper(this);
        childHelper.setNestedScrollingEnabled(true);
    }

    public void startScrollerTask() {
        if (mScrollerTask == null) {
            mScrollerTask = new Handler(WXThread.secure(this));
        }
        mInitialPosition = getScrollY();
        mScrollerTask.sendEmptyMessageDelayed(0, mCheckTime);
    }

    public WXScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public WXScrollView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setOverScrollMode(View.OVER_SCROLL_NEVER);
    }

    /**
     * Add listener for scrollView.
     */
    public void addScrollViewListener(WXScrollViewListener scrollViewListener) {
        if (!mScrollViewListeners.contains(scrollViewListener)) {
            mScrollViewListeners.add(scrollViewListener);
        }
    }

    public void removeScrollViewListener(WXScrollViewListener scrollViewListener) {
        mScrollViewListeners.remove(scrollViewListener);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            mRedirectTouchToStickyView = true;
        }

        if (mRedirectTouchToStickyView) {
            mRedirectTouchToStickyView = mCurrentStickyView != null;

            if (mRedirectTouchToStickyView) {
                mRedirectTouchToStickyView = ev.getY() <= mCurrentStickyView.getHeight()
                        && ev.getX() >= mCurrentStickyView.getLeft()
                        && ev.getX() <= mCurrentStickyView.getRight();
            }
        }

        if (mRedirectTouchToStickyView) {
            if (mScrollRect == null) {
                mScrollRect = new Rect();
                getGlobalVisibleRect(mScrollRect);
            }
            mCurrentStickyView.getLocationOnScreen(stickyViewP);
            ev.offsetLocation(0, stickyViewP[1] - mScrollRect.top);
        }
        boolean result = super.dispatchTouchEvent(ev);
        if (wxGesture != null) {
            result |= wxGesture.onTouch(this, ev);
        }
        return result;
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mCurrentStickyView != null) {
            canvas.save();
            mCurrentStickyView.getLocationOnScreen(mStickyP);
            int realOffset = (mStickyOffset <= 0 ? mStickyOffset : 0);
            canvas.translate(mStickyP[0], getScrollY() + realOffset);
            canvas.clipRect(0, realOffset, mCurrentStickyView.getWidth(),
                    mCurrentStickyView.getHeight());
            mCurrentStickyView.draw(canvas);
            canvas.restore();
        }
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (!scrollable) {
            return true; // when scrollable is set to false, then eat the touch event
        }
        if (mRedirectTouchToStickyView) {

            if (mScrollRect == null) {
                mScrollRect = new Rect();
                getGlobalVisibleRect(mScrollRect);
            }
            mCurrentStickyView.getLocationOnScreen(stickyViewP);
            ev.offsetLocation(0, -(stickyViewP[1] - mScrollRect.top));
        }

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            mHasNotDoneActionDown = false;
        }

        if (mHasNotDoneActionDown) {
            MotionEvent down = MotionEvent.obtain(ev);
            down.setAction(MotionEvent.ACTION_DOWN);
            mHasNotDoneActionDown = false;
            down.recycle();
        }

        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            ox = ev.getX();
            oy = ev.getY();
            // Dispatch touch event to parent view
            startNestedScroll(ViewCompat.SCROLL_AXIS_HORIZONTAL | ViewCompat.SCROLL_AXIS_VERTICAL);
        }

        if (ev.getAction() == MotionEvent.ACTION_UP || ev.getAction() == MotionEvent.ACTION_CANCEL) {
            mHasNotDoneActionDown = true;
            // stop nested scrolling dispatch
            stopNestedScroll();
        }

        if (ev.getAction() == MotionEvent.ACTION_MOVE) {
            float clampedX = ev.getX();
            float clampedY = ev.getY();
            int dx = (int) (ox - clampedX);
            int dy = (int) (oy - clampedY);

            if (dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)) {
                // sub dx/dy was consumed by parent view!!!
                ev.setLocation(clampedX + consumed[0], clampedY + consumed[1]);
            }
            ox = ev.getX();
            oy = ev.getY();
        }

        return super.onTouchEvent(ev);
    }

    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        childHelper.setNestedScrollingEnabled(enabled);
    }

    @Override
    public boolean isNestedScrollingEnabled() {
        return childHelper.isNestedScrollingEnabled();
    }

    @Override
    public boolean startNestedScroll(int axes) {
        return childHelper.startNestedScroll(axes);
    }

    @Override
    public void stopNestedScroll() {
        childHelper.stopNestedScroll();
    }

    @Override
    public boolean hasNestedScrollingParent() {
        return childHelper.hasNestedScrollingParent();
    }

    public boolean isScrollable() {
        return scrollable;
    }

    public void setScrollable(boolean scrollable) {
        this.scrollable = scrollable;
    }

    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) {
        return childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return childHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return childHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

    @Override
    public boolean onNestedPreFling(View target, float velocityX,
                                    float velocityY) {
        return dispatchNestedPreFling(velocityX, velocityY);
    }

    @Override
    public boolean onNestedFling(View target, float velocityX, float velocityY,
                                 boolean consumed) {
        return dispatchNestedFling(velocityX, velocityY, consumed);
    }

    @Override
    public void fling(int velocityY) {
        super.fling(velocityY);
        if (mScrollerTask != null) {
            mScrollerTask.removeMessages(0);
        }
        startScrollerTask();
    }

    @Override
    protected void onScrollChanged(int x, int y, int oldx, int oldy) {
        mScrollX = getScrollX();
        mScrollY = getScrollY();
        onScroll(WXScrollView.this, mScrollX, mScrollY);
        View view = getChildAt(getChildCount() - 1);
        if (view == null) {
            return;
        }
        int d = view.getBottom();
        d -= (getHeight() + mScrollY);
        if (d == 0) {
            onScrollToBottom(mScrollX, mScrollY);
        }
        int count = mScrollViewListeners == null ? 0 : mScrollViewListeners.size();
        for (int i = 0; i < count; ++i) {
            mScrollViewListeners.get(i).onScrollChanged(this, x, y, oldx, oldy);
        }

        showStickyView();
    }

    protected void onScroll(WXScrollView scrollView, int x, int y) {
        int count = mScrollViewListeners == null ? 0 : mScrollViewListeners.size();
        for (int i = 0; i < count; ++i) {
            mScrollViewListeners.get(i).onScroll(this, x, y);
        }
    }

    protected void onScrollToBottom(int x, int y) {
        int count = mScrollViewListeners == null ? 0 : mScrollViewListeners.size();
        for (int i = 0; i < count; ++i) {
            mScrollViewListeners.get(i).onScrollToBottom(this, x, y);
        }
    }

    private void showStickyView() {
        if (mWAScroller == null) {
            return;
        }
        View curStickyView = procSticky(mWAScroller.getStickMap());

        if (curStickyView != null) {
            mCurrentStickyView = curStickyView;
        } else {
            mCurrentStickyView = null;
        }
    }

    private View procSticky(Map<String, Map<String, WXComponent>> mStickyMap) {
        if (mStickyMap == null) {
            return null;
        }
        Map<String, WXComponent> stickyMap = mStickyMap.get(mWAScroller.getRef());
        if (stickyMap == null) {
            return null;
        }

        Iterator<Entry<String, WXComponent>> iterator = stickyMap.entrySet().iterator();
        Entry<String, WXComponent> entry = null;
        WXComponent stickyData;
        while (iterator.hasNext()) {
            entry = iterator.next();
            stickyData = entry.getValue();

            getLocationOnScreen(stickyScrollerP);
            stickyData.getHostView().getLocationOnScreen(stickyViewP);
            int parentH = 0;
            if (stickyData.getParent() != null && stickyData.getParent().getRealView() != null) {
                parentH = stickyData.getParent().getRealView().getHeight();
            }
            int stickyViewH = stickyData.getHostView().getHeight();
            int stickyShowPos = stickyScrollerP[1];
            int stickyStartHidePos = -parentH + stickyScrollerP[1] + stickyViewH;
            if (stickyViewP[1] <= stickyShowPos && stickyViewP[1] >= (stickyStartHidePos - stickyViewH)) {
                mStickyOffset = stickyViewP[1] - stickyStartHidePos;
                stickyData.setStickyOffset(stickyViewP[1] - stickyScrollerP[1]);
                return stickyData.getHostView();
            } else {
                stickyData.setStickyOffset(0);
            }
        }
        return null;
    }

    @Override
    public boolean handleMessage(Message msg) {
        switch (msg.what) {
            case 0:
                if (mScrollerTask != null) {
                    mScrollerTask.removeMessages(0);
                }
                int newPosition = getScrollY();
                if (mInitialPosition - newPosition == 0) {//has stopped
                    onScrollStopped(WXScrollView.this, getScrollX(), getScrollY());
                } else {
                    onScroll(WXScrollView.this, getScrollX(), getScrollY());
                    mInitialPosition = getScrollY();
                    if (mScrollerTask != null) {
                        mScrollerTask.sendEmptyMessageDelayed(0, mCheckTime);
                    }
                }
                break;
            default:
                break;
        }
        return true;
    }

    protected void onScrollStopped(WXScrollView scrollView, int x, int y) {
        int count = mScrollViewListeners == null ? 0 : mScrollViewListeners.size();
        for (int i = 0; i < count; ++i) {
            mScrollViewListeners.get(i).onScrollStopped(this, x, y);
        }
    }

    @Override
    public void destroy() {
        if (mScrollerTask != null) {
            mScrollerTask.removeCallbacksAndMessages(null);
        }
    }

    @Override
    public void registerGestureListener(WXGesture wxGesture) {
        this.wxGesture = wxGesture;
    }

    @Override
    public WXGesture getGestureListener() {
        return wxGesture;
    }

    public Rect getContentFrame() {
        return new Rect(0, 0, computeHorizontalScrollRange(), computeVerticalScrollRange());
    }

    public interface WXScrollViewListener {

        void onScrollChanged(WXScrollView scrollView, int x, int y, int oldx, int oldy);

        void onScrollToBottom(WXScrollView scrollView, int x, int y);

        void onScrollStopped(WXScrollView scrollView, int x, int y);

        void onScroll(WXScrollView scrollView, int x, int y);
    }

    public void setWAScroller(WXScroller mWAScroller) {
        this.mWAScroller = mWAScroller;
    }
}
